crackmeUK 解析手引き その2(easy)


今回は EASY についての解説です。

前回デバッガチェック部分の解説を行いましたが、デバッガチェックを回避するパッチは
すでに当てていますか?そうしないと解析ができません。
未だの方は、解析手引き その1 を御覧になり、パッチを当ててから進んで下さい。


crackme を起動してみると、マシンによって固有 ID が生成されるようですが、
まずこの ID はどのように生成されているかを調べてみる必要がありそうです。
マシン固有 ID の生成には様々な方法がありますが、今回は『eagle0wl's crackme VOL.01』の crackme #10 で
固有 ID 生成のために用いられていた GetVolumeInformationA にブレークポイントを仕掛けます。


右クリック→「検索」→「ラベル一覧」(「Search for」→「Name(Label)」)で、ラベル一覧の一覧を表示します。


Names in CrackMe
Address    Section    Type    (  Name                                    Comment
00404E52   .text      Export     <ModuleEntryPoint>
0041C000   .rdata     Import  (  ADVAPI32.RegCreateKeyExA
0041C004   .rdata     Import  (  ADVAPI32.RegCloseKey
0041C008   .rdata     Import  (  ADVAPI32.RegOpenKeyExA
0041C00C   .rdata     Import  (  ADVAPI32.RegSetValueExA
0041C014   .rdata     Import  (  COMCTL32.#17
(〜中略)
0041C170   .rdata     Import  (  KERNEL32.GetWindowsDirectoryA
0041C174   .rdata     Import  (  KERNEL32.GetVolumeInformationA
0041C178   .rdata     Import  (  KERNEL32.GetModuleHandleA
0041C17C   .rdata     Import  (  KERNEL32.GetModuleFileNameA
0041C180   .rdata     Import  (  KERNEL32.CreateFileA
(〜中略)
0041C46C   .rdata     Import  (  USER32.HideCaret
0041C474   .rdata     Import     WINSPOOL.DocumentPropertiesA
0041C478   .rdata     Import     WINSPOOL.OpenPrinterA
0041C47C   .rdata     Import     WINSPOOL.ClosePrinter
0041C484   .rdata     Import  (  comdlg32.GetFileTitleA


「0041C174   .rdata     Import  (  KERNEL32.GetVolumeInformationA」を選択して、「Enter」キーを押してください。
「References in CrackMe:.text to KERNEL32.GetVolumeInformationA」というウィンドウが表示されます。


References in CrackMe:.text to KERNEL32.GetVolumeInformationA
Address    Disassembly                               Comment
0040181D   CALL DWORD PTR DS:[<&KERNEL32.GetVolumeI  kernel32.GetVolumeInformationA
0041445E   CALL DWORD PTR DS:[<&KERNEL32.GetVolumeI  kernel32.GetVolumeInformationA


とりあえず両方にBPを仕掛けてください。
「F9」キーで実行させます。

すると 0041445E でブレークしました。画面右下のスタックウィンドウを見て下さい。
* スタックウィンドウのアドレスは環境(OS)によって変わりますので注意して下さい。


0065F58C   00884250  |RootPathName = "C:\"
0065F590   00000000  |VolumeNameBuffer = NULL
0065F594   00000000  |MaxVolumeNameSize = 0
0065F598   00000000  |pVolumeSerialNumber = NULL
0065F59C   0065F6F4  |pMaxFilenameLength = 0065F6F4
0065F5A0   0065F6FC  |pFileSystemFlags = 0065F6FC
0065F5A4   00000000  |pFileSystemNameBuffer = NULL
0065F5A8   00000000  \pFileSystemNameSize = NULL


これを見る限り、NULL になっているので、マシン固有情報を取っていないようです。
F9 キーで飛ばしましょう。
すると再び同じ所で停止しますがもう一度 F9 を押して下さい。
すると『This is "Crack Me". Created By UK』という表示が出ました。
OK ボタンを押すと 0040181D でブレークしました。スタックウィンドウを見てみましょう。


0065F5C8   0065F604  |RootPathName = "C:\"
0065F5CC   00000000  |VolumeNameBuffer = NULL
0065F5D0   00000000  |MaxVolumeNameSize = 0
0065F5D4   0065FD6C  |pVolumeSerialNumber = 0065FD6C    ; あやしい
0065F5D8   00000000  |pMaxFilenameLength = NULL
0065F5DC   00000000  |pFileSystemFlags = NULL
0065F5E0   00000000  |pFileSystemNameBuffer = NULL
0065F5E4   00000000  \pFileSystemNameSize = NULL


NULL が続く中、pVolumeSerialNumber 【だけ】に(ローカル変数の)アドレスが入っています。
(このアドレスは環境によって変化するので各自頭の中で置き換えてください)
非常に怪しいですね。画面右下のダンプウィンドウにフォーカスを合わせ、Ctrl + G から
0065FD6C (この値は各自置き換えること)と入力して、該当部分を参照して下さい。

次に F8 を押して GetVolumeInformationA を押して実行すると、このアドレスに値が入ります。
0065FD6C  98 56 12 AB        ←この値は一例です。
          ^^^^^^^^^^^        
取得された値は AB125698h であることがわかります。

さて、表示された ID はというと、 AB-125698 というようにほぼそのままの状態になっているはずです。
ちなみに値を取得した直後にこの値を書き換えると ID も変わりました。
ということは、VolumeSerialNumber から ID を生成しているとほぼ確定されたということになります。
とりあえず、固有 ID 生成部分については終わりです。




続いてキーチェックの方へ行ってみたいと思います。
今回は EASY について解説ですので、ラジオボタンの EASY にチェックが入っているか確認してください。

次に、パスワード入力欄にフェイクパス "9876543210" を入力した後に
定番 API 、GetWindowsTextA にブレークポイントを仕掛けてから OK ボタンを押してみましょう。


00415460 |. 50             push    eax                                        ; |Buffer
00415461 |. FF76 1C        push    dword ptr ds:[esi+1C]                      ; |hWnd
00415464 |. FF15 30C44100  call    near dword ptr ds:[<&USER32.GetWindowTextA>; \GetWindowTextA


実はこの命令の後、別の処理で 19 回 GetWindowTextA が呼び出されているのですが、
この API コールで本当に入力パスが取得されるのか確認してみることにしましょう。

画面右下のスタックウィンドウを見て下さい。


0065F790   000003F8  |hWnd = 000003F8 (class='Edit',wndproc=807ECAA8,parent=00000FF8)
0065F794   0088C844  |Buffer = 0088C844        <-- 入力パスが格納されるバッファのアドレス
0065F798   00000009  \Count = 9


まだ GetWindowTextA は実行されていません。入力パスが格納されるバッファのアドレスが
上から2番目の列に見えます。

この解説は、WindowsMe 上で解析を行って書かれたものを編集しているのですが、バッファのアドレスは
環境によって変わりますので各自置き換えて読んで下さい。

それでは、画面左下のダンプウィンドウにフォーカスを合わせてから、Ctrl + G を押して
バッファのアドレスを打ち込んでジャンプしましょう。
ジャンプできましたか?

それでは F8 キーを押して GetWindowTextA を実行して下さい。
するとバッファに入力パスが表示されました。チェックルーチンはこの先にありそうです。

ちなみにダンプウィンドウの表示アドレスはそのままにして F9 を連打すると(今はしないでね)、
そのたびにブレークするわけですが(計 19 回)、登録失敗時に表示される『Incorrect Password』の
文字列が踊る様子を見ることが出来て面白いです。


本題に戻ります。GetWindowTextA を抜けたところで、call 命令がひとつありますが、
構わず F8 キーで飛ばして ret 命令 (00415484) まで進めていきましょう。
ret 命令でジャンプするとアドレス 00401A7E に出るはずです。

00401A7E  |. 8B4C24 10      mov     ecx, dword ptr ss:[esp+10]

さて、ここからが実質的なキーチェックルーチンです。気を引き締めてかかりましょう。

いきなりアドレス 00401A88 で GetTickCount 命令が呼び出されています。
起動時のデバッガチェックで GetTickCount が使用されていたことを思い出して下さい。
どうやらキーチェック部分でも同様のチェッカが存在しているようです。

そんなことを思い出しながら F8 キーを軽快に叩いていきましょう。
まもなく以下のような比較分岐命令が現れてきました。


00401A96  |. 8B41 F8        mov     eax, dword ptr ds:[ecx-8]  ; eax = 入力パスの文字数
00401A99  |. 83F8 10        cmp     eax, 10                    ; 入力パスの文字数は 16 文字か?
00401A9C  |. 75 60          jnz     short CRACKME.00401AFE     ; 16 文字以外なら登録失敗
00401A9E  |. 8039 43        cmp     byte ptr ds:[ecx], 43      ; 入力パス一文字目は 43h = 'C' か?
00401AA1  |. 75 0B          jnz     short CRACKME.00401AAE
00401AA3  |. 8079 01 52     cmp     byte ptr ds:[ecx+1], 52    ; 入力パス二文字目は 52h = 'R' か?
00401AA7  |. 75 05          jnz     short CRACKME.00401AAE
00401AA9  |. C64424 14 01   mov     byte ptr ss:[esp+14], 1    ; 1を [esp+14] に入れているが、、、


まず入力パスの文字数チェックを行っています。文字数を取得できる API が近くに見あたらないのでわかりにくいですが、
パスチェックを行う上で最初にやることは文字数チェックと相場が決まっていますので、そう予測しました。
不安なら後で文字数を変えて再度入力して確認してみてください。

次に入力パスの先頭2文字が "CR" であるかどうかをチェックしているわけですが、ひとまずゼロフラグを反転して続けましょう。
無事通過すると、なぜか1を [esp+14] (ローカル変数)に入れていて非常に怪しいです。
([esp+14] はアドレス 00401A67 にて初期化されている)。ここは要チェックですね。

次にアドレス 00401AB6 に call 命令がありますがとりあえず F8 で飛ばしてみましょう。


00401AAE  |> 6A 02          push    2                          ; /Arg2 = 00000002  ; 削りたい文字数
00401AB0  |  6A 00          push    0                          ; |Arg1 = 00000000  ; 何文字目から削るか
00401AB2  |. 8D4C24 18      lea     ecx, dword ptr ss:[esp+18] ; |
00401AB6  |. E8 47010100    call    CRACKME.00411C02           ; \CRACKME.00411C02


call 命令通過後、 ecx レジスタを見て下さい。[ecx] = "76543210" というように、先頭2文字が削れています。
前回、「殆どのコンパイラは関数の戻り値を eax レジスタに格納している」と説明しましたが、今回戻り値を格納するために
ecx レジスタを使用したという訳ではありません。
ecx レジスタに偶然そういう値が入っただけに過ぎません。詳しくは自分で call 内部に潜って確認してみて下さい。

続けてトレースしていきましょう。


00401ABB  |. 8B8E D0000000  mov     ecx, dword ptr ds:[esi+D0] ; 固有 ID 値を ecx レジスタに
00401AC1  |. 33FF           xor     edi, edi                   ; ループに入る前に edi を初期化している
00401AC3  |. BE 01000000    mov     esi, 1                     ; esi も初期化している模様
00401AC8  |> 8BC1           /mov     eax, ecx  <-----------------+
00401ACA  |. 33D2           |xor     edx, edx                    |
00401ACC  |. BB 0A000000    |mov     ebx, 0A                     |
00401AD1  |. F7F3           |div     ebx                         |
00401AD3  |. B8 CDCCCCCC    |mov     eax, CCCCCCCD               |
00401AD8  |. 03FA           |add     edi, edx                    |
00401ADA  |. 42             |inc     edx                         |
00401ADB  |. 0FAFF2         |imul    esi, edx                    |
00401ADE  |. F7E1           |mul     ecx                         |
00401AE0  |. C1EA 03        |shr     edx, 3                      |
00401AE3  |. 8BCA           |mov     ecx, edx                    | ループして何やら値を
00401AE5  |.^75 E1          \jnz     short CRACKME.00401AC8  ----+ 生成している模様


固有 ID 値(表示が 12-AB2345 なら値は 12AB2345 となる)から何やら値を生成しているようです。
このループを見てどのように値を生成しているわかりますか?

よく見てみるとループ内ではレジスタ間でのやり取りだけでスタックポインタ(ローカル変数・引数)は参照されていません。
したがって生成された値はレジスタに入ってるはずです。

ではどのレジスタに値が入っているのか。実はループ内部の処理を追いかける必要はありません。
ループ前に xor  edi, edi    mov  esi, 1 というように edi, esi レジスタが初期化されていることに注目して下さい。
変数に値を格納するときは前もってその変数の値を初期化する必要があります。
ということは、edi, esi レジスタに生成された値が格納されているとほぼ断定できます。

前回、ebx, esi, edi レジスタの値は call 命令内部で push'n pop されているため値は保持されます。
よって そのレジスタに入った値は結構重要だということをお話ししました。
そのことを頭に入れておきつつ、続けていきましょう。


00401AE7  |. 6A 00          push    0
00401AE9  |. 68 DC214200    push    CRACKME.004221DC           ; [004221DC] = "-" (ハイフン)
00401AEE  |. 8D4C24 18      lea     ecx, dword ptr ss:[esp+18]
00401AF2  |. E8 0C030100    call    CRACKME.00411E03           ; あやしげな関数
00401AF7  |. 8BE8           mov     ebp, eax   ; 戻り値を ebp レジスタに格納
00401AF9  |. 83FD FF        cmp     ebp, -1    ; (ebp = call 命令の戻り値) が -1 以外なら OK
00401AFC  |. 75 1B          jnz     short CRACKME.00401B19  --> ジャンプすることで継続


この call ではパスワード文字列 "76543210" の中からハイフンの位置を探しています。
なぜハイフンの位置を探す関数であることがわかるのかと言いますと、call 内部をトレースして
戻り値が -1 以外ならOK ということですが、戻り値に -1 が返ることのある文字列処理系の関数というと
文字列比較(s1 < s2 の時)、文字列検索(該当文字列が見つからない時)当たりでしょうか。
さらにプッシュされている値を見ると、

00401AE9  |. 68 DC214200    push    CRACKME.004221DC           ; [004221DC] = "-" (ハイフン)

この行に、Dump ウィンドウにフォーカスを合わせ、Ctrl + G | 004221DC と打ってみると、該当部分には
ハイフン一文字がありました。となると、ハイフンを探す命令ではないかと推測できるというわけです。

ここで確認、入力したフェイクパスは "9876543210" です。ハイフンはありません。
仕掛けてあるブレークポイントを全て解除してから、call 命令(00401AF2)に BP を仕掛けた後、
一旦登録に失敗してから再度フェイクパス "CR9876-543210987" と打ち込んでもう一度登録ボタンを押してみましょう。

すると今仕掛けた BP でブレークしました。続けてトレースしていきましょう。
今度はハイフンがあるので 00401B19 にジャンプして処理が続きます。

00401B23 に call 命令がありますが、引数は入力パスとは関係なさそうな上、call 命令の直後に比較分岐命令がないので、
素通りしていきましょう。
00401B32 で再び call 命令です。


00401B2E  |. 6A 10          push    10
00401B30  |. 51             push    ecx
00401B31  |. 50             push    eax                ; [eax] = "9876"
00401B32  |. E8 302F0000    call    CRACKME.00404A67   ; あやしい call
00401B37  |. 83C4 0C        add     esp, 0C
00401B3A  |. 3BF8           cmp     edi, eax  ; edi = 固有IDから生成された値, eax = 関数戻り値
00401B3C  |. 8D4C24 1C      lea     ecx, dword ptr ss:[esp+1C]
00401B40  |. 0F94C3         sete    bl


3文字目からハイフン直前までの文字列のポインタがプッシュされています。
さらに call を抜けた後に比較命令があり、 edi レジスタ、これは固有IDから生成された値です。
非常に怪しいです。F8 で call を抜けてみると、その戻り値は eax = 00009876 となっています。

ここは、文字列([0-9][A-F][a-f])から値に変換するものでした。
ということは、edi の値に合わせて入力パスを変えれば OK です。
(edi = 2D なら、"CR002D-543210987" といった具合に変える)


00401B3A  |. 3BF8           cmp     edi, eax                   ; 比較した結果
00401B3C  |. 8D4C24 1C      lea     ecx, dword ptr ss:[esp+1C] 
00401B40  |. 0F94C3         sete    bl                         ; 等しければ bl = 1, 等しくないのなら bl = 0 が入る
00401B43  |. E8 CA590100    call    CRACKME.00417512           ; call 命令があるが、ebx レジスタの値は保持される
00401B48  |. 84DB           test    bl, bl                     ; bl = 1 なら
00401B4A  |. 74 05          je      short CRACKME.00401B51     ; ここでジャンプしない
00401B4C  |. C64424 16 01   mov     byte ptr ss:[esp+16], 1    ; またローカル変数に 1 を代入している
00401B51  |> 45             inc     ebp


比較した結果が等しければ、00401B4C でまたローカル変数に 1 を代入しています。この 1 は TRUE で、
最後にまとめてチェックするのでは無いかと予想できます。


00401B52  |. 8D4C24 10      lea     ecx, dword ptr ss:[esp+10]
00401B56  |. 55             push    ebp                              ; /Arg2
00401B57  |. 6A 00          push    0                                ; |Arg1 = 00000000
00401B59  |. E8 A4000100    call    CRACKME.00411C02                 ; \CRACKME.00411C02


call を抜けるとレジスタの値は eax = 0000000E, [ecx] = "543210987" となっていました。
これはハイフン以降の文字列です。 ホントは call 内部もトレースするべきなのですが、
解析時間の短縮のためにもこういった予想外の情報も活用するべきです。


00401B5E  |. 6A 00          push    0
00401B60  |. 68 DC214200    push    CRACKME.004221DC            ; [004221DC] = "-"(ハイフン)
00401B65  |. 8D4C24 18      lea     ecx, dword ptr ss:[esp+18]
00401B69  |. E8 95020100    call    CRACKME.00411E03            ; ハイフンチェック再び
00401B6E  |. 83CF FF        or      edi, FFFFFFFF               ; これは mov  edi, FFFFFFFF と同じ
00401B71  |. 3BC7           cmp     eax, edi                    ; ハイフンが見つからない(eax = FFFFFFFF)と
00401B73  |. 0F84 8B000000  je      CRACKME.00401C04            ; ジャンプして登録失敗


またハイフンチェックです。
現在仕掛けられているブレークポイントを解除した後、 call 部分にブレークポイントを仕掛け、
"CR002D-543210987" を "CR002D-5432-1098" とでも変えて再び試してみましょう。
(文字数は16文字であること、002D は固有 ID から生成された値からであることに注意。002D の部分は各自置き換えて下さい)
もう少しです。頑張りましょう。


00401B79  |. 40             inc     eax
00401B7A  |. 8D4C24 10      lea     ecx, dword ptr ss:[esp+10]
00401B7E  |. 50             push    eax                              ; /Arg2 
00401B7F  |. 6A 00          push    0                                ; |Arg1 = 00000000
00401B81  |. E8 7C000100    call    CRACKME.00411C02                 ; \CRACKME.00411C02
00401B86  |. 8B4424 10      mov     eax, dword ptr ss:[esp+10]       ; [esp+10] は ecx と同じ値
00401B8A  |. 8D5424 20      lea     edx, dword ptr ss:[esp+20]


call を抜けた後のレジスタの値は eax = 00000009, [ecx] = "1098" となっています。1098 は
"CR002D-5432-1098"
             ^^^^ この部分ですね。
直後の mov  eax, dword ptr ss:[esp+10] で同じ値が代入されています。


00401B8E  |. 6A 10          push    10
00401B90  |. 52             push    edx
00401B91  |. 50             push    eax                        ; [eax] = "1098"
00401B92  |. E8 D02E0000    call    CRACKME.00404A67           ; 文字列([0-9][A-F][a-f])から値に変換する
00401B97  |. 83C4 0C        add     esp, 0C
00401B9A  |. 3BF0           cmp     esi, eax                   ; esi = 固有IDから生成された値, eax = 関数戻り値
00401B9C  |. B3 01          mov     bl, 1                      ; bl に 1 が代入される
00401B9E  |. 74 04          je      short CRACKME.00401BA4     ; esi = eax ならばジャンプ  -----------------+
00401BA0  |. 8A5C24 17      mov     bl, byte ptr ss:[esp+17]   ; [esp+17] は 0 (00401A5A で初期化されている)|
00401BA4  |> FF15 24C24100  call    near dword ptr ds:[<&KERNEL32.GetTickCount>; [GetTickCount]  <---------+
00401BAA  |. 8A4C24 14      mov     cl, byte ptr ss:[esp+14]   ; 入力パス先頭2文字は
00401BAE  |. 84C9           test    cl, cl                     ; 'CR' だったか
00401BB0  |. 74 52          je      short CRACKME.00401C04
00401BB2  |. 8A4C24 16      mov     cl, byte ptr ss:[esp+16]   ; "CRxxxx-yyyy-zzzzz"
00401BB6  |. 84C9           test    cl, cl                     ; xxxx の比較の結果
00401BB8  |. 74 4A          je      short CRACKME.00401C04
00401BBA  |. 84DB           test    bl, bl                     ; esi, eax の比較の結果、
00401BBC  |. 74 46          je      short CRACKME.00401C04     ; 等しければジャンプしない
00401BBE  |. 2B4424 24      sub     eax, dword ptr ss:[esp+24] ; キーチェック部分の実行時間算出
00401BC2  |. 83F8 64        cmp     eax, 64                    ; 100 ミリ秒以内なら
00401BC5  |. 73 3D          jnb     short CRACKME.00401C04     ; ジャンプしない
00401BC7  |. 68 00224200    push    CRACKME.00422200                  ;  ASCII "Congratulations!"


最後は比較分岐が非常に多く、入り組んだ形となっています。
キーチェック部分で mov  byte ptr ss:[esp+**], 1  というような命令が何度かありましたが、
これは入力パスとを比較後、すぐに不正解処理に飛ばさずに一旦ローカル変数に比較結果を
正解 = 1 (TRUE), 不正解 = 0 (FALSE)として保存し、最後にまとめてチェックしているためです。
コードの右にコメントを入れたのでじっくりと読んで下さい。
一番最後に、


00401BA4  |> FF15 24C24100  call    near dword ptr ds:[<&KERNEL32.GetTickCount>; [GetTickCount]
(中略)
00401BBE  |. 2B4424 24      sub     eax, dword ptr ss:[esp+24] ; キーチェック部分の実行時間算出
00401BC2  |. 83F8 64        cmp     eax, 64                    ; 100 ミリ秒以内なら


デバッガチェック部分にあったトレースチェッカと同じものですね。前回説明したので自分で理解して下さい。
これより、以下のことがわかりました。

・固有 ID は GetVolumeInformationA の pVolumeSerialNumber から取得。
 pVolumeSerialNumber = 54AB32CD なら 54-AB32CD と表示されます。
・正解パスは16文字
・正解パスの形式は CRxxxx-yyyy-zzzz で、ハイフンが2ヶ所あり、xxxx,yyyy,zzzz には
 [0-9][A-F][a-f] の範囲で文字が入り、xxxx,yyyy,zzzz 各部分の文字列長は決まっていません。
 (全体が16文字であればよい)。

・アドレス 00401ABB - 00401AE5 の間のループで命令固有 ID から2つの値を算出し、
 その値はedi, esi レジスタにそれぞれ入ります。
・xxxx と edi の値を比較。edi = 00000012 であれば xxxx は "12" または"00012" といった風になります。
 同様に zzzz と esi の値を比較。 esi = 0001AB49 であればzzzz は "1AB49" または "01AB49" といった風になります。
 yyyy の比較命令は無いのでここは任意となります。ただしこの部分にハイフンは使えません(フォーマット違反)。


■まとめ

アドレス 00401AE7 にブレークポイントを仕掛けて OK ボタンを押すとブレークするはずなので、
その状態での edi レジスタと esi レジスタの値を読み、その値が edi = 0000012, esi = 0001AB49 だとすると、
正解パスは"CR12-xxxx-01AB49", "CR000012-x-1AB49", "CR0012--0001AB49", などが考えられます。
(x についてはハイフン以外の任意の文字)
cr1111-1111-1111でもトレースで,edi,esiから正解パスが出てきます。